package com.starry.core.web.core.logger.aspect;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.starry.core.common.domain.R;
import com.starry.core.common.enums.UserTypeEnum;
import com.starry.core.common.utils.ServletUtils;
import com.starry.core.common.utils.UserAgentParser;
import com.starry.core.ip.core.utils.IPUtils;
import com.starry.core.security.context.SecurityInfoContext;
import com.starry.core.web.config.web.WebProperties;
import com.starry.core.web.core.logger.annotation.OperationLog;
import com.starry.core.web.core.logger.interceptor.ApiAccessLogInterceptor;
import com.starry.module.system.api.logger.SysLogOperationApi;
import com.starry.module.system.api.logger.domain.qo.SysLogOperationQo;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;

import java.time.Instant;
import java.time.LocalDateTime;
import java.util.*;

/**
 * 访问日志记录处理
 *
 * @author xiaoke
 */
@Slf4j
@Aspect
@Component
public class OperationLogAspect {

    private static final String[] SANITIZE_KEYS = new String[]{"password", "token", "accessToken", "refreshToken"};
    /**
     * 计算操作消耗时间
     */
    private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time");
    private final HttpServletRequest httpServletRequest;
    private final WebProperties webProperties;
    private final SysLogOperationApi sysLogOperationApi;
    @Value("${spring.application.name}")
    private String applicationName;

    public OperationLogAspect(HttpServletRequest httpServletRequest, WebProperties webProperties, SysLogOperationApi sysLogOperationApi) {
        this.httpServletRequest = httpServletRequest;
        this.webProperties = webProperties;
        this.sysLogOperationApi = sysLogOperationApi;
    }

    private static String sanitizeMap(Map<String, ?> map, String[] sanitizeKeys) {
        if (CollUtil.isEmpty(map)) {
            return null;
        }
        if (sanitizeKeys != null) {
            MapUtil.removeAny(map, sanitizeKeys);
        }
        MapUtil.removeAny(map, SANITIZE_KEYS);
        return JSONUtil.toJsonStr(map);
    }

    private static String sanitizeJson(String jsonString, String[] sanitizeKeys) {
        if (StrUtil.isEmpty(jsonString)) {
            return null;
        }
        try {
            JsonNode rootNode = JSONUtil.toBean(jsonString, JsonNode.class);
            sanitizeJson(rootNode, sanitizeKeys);
            return JSONUtil.toJsonStr(rootNode);
        } catch (Exception e) {
            // 脱敏失败的情况下，直接忽略异常，避免影响用户请求
            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
            return jsonString;
        }
    }

    private static String sanitizeJson(R<?> r, String[] sanitizeKeys) {
        if (r == null) {
            return null;
        }
        String jsonString = JSONUtil.toJsonStr(r);
        try {
            JsonNode rootNode = JSONUtil.toBean(jsonString, JsonNode.class);
            sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段，不处理 code、msg 字段，避免错误被脱敏掉
            return JSONUtil.toJsonStr(rootNode);
        } catch (Exception e) {
            // 脱敏失败的情况下，直接忽略异常，避免影响用户请求
            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
            return jsonString;
        }
    }

    private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) {
        // 情况一：数组，遍历处理
        if (node.isArray()) {
            for (JsonNode childNode : node) {
                sanitizeJson(childNode, sanitizeKeys);
            }
            return;
        }
        // 情况二：非 Object，只是某个值，直接返回
        if (!node.isObject()) {
            return;
        }
        //  情况三：Object，遍历处理
        Iterator<Map.Entry<String, JsonNode>> iterator = node.properties().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, JsonNode> entry = iterator.next();
            if (ArrayUtil.contains(sanitizeKeys, entry.getKey())
                    || ArrayUtil.contains(SANITIZE_KEYS, entry.getKey())) {
                iterator.remove();
                continue;
            }
            sanitizeJson(entry.getValue(), sanitizeKeys);
        }
    }

    /**
     * 处理请求前执行
     */
    @Before(value = "@annotation(operationLog)")
    public void boBefore(JoinPoint joinPoint, OperationLog operationLog) {
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "@annotation(operationLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, OperationLog operationLog, Object jsonResult) {
        handleLog(joinPoint, operationLog, jsonResult);
    }

    protected void handleLog(final JoinPoint joinPoint, OperationLog operationLog, Object jsonResult) {
        HandlerMethod handlerMethod = (HandlerMethod) httpServletRequest.getAttribute(ApiAccessLogInterceptor.ATTRIBUTE_HANDLER_METHOD);

        try {
            SysLogOperationQo operation = new SysLogOperationQo();

            //设置操作时间
            Instant instant = Instant.ofEpochMilli(TIME_THREADLOCAL.get());
            operation.setOperTime(LocalDateTime.ofInstant(instant, TimeZone.getDefault().toZoneId()));


            operation.setApplicationName(applicationName);

            // ip
            operation.setRequestIp(IPUtils.getIpAddr(httpServletRequest));
            try {
                operation.setIpAddress(IPUtils.getArea(operation.getRequestIp()).getName());
            } catch (Exception ignored) {
            }

            //用户信息
            operation.setOperId(SecurityInfoContext.getUserId());
            operation.setOperName(SecurityInfoContext.getUsername());
            operation.setUserType(getLoginUserType(httpServletRequest));

            //请求
            operation.setRequestUrl(httpServletRequest.getRequestURI());
            operation.setRequestMethod(httpServletRequest.getMethod());

            //请求体
            if (operationLog.requestEnable()) {
                Map<String, String> queryString = ServletUtils.getParamMap(httpServletRequest);
                String requestBody = ServletUtils.isJsonRequest(httpServletRequest) ? ServletUtils.getBody(httpServletRequest) : null;
                Map<String, Object> requestParams = MapUtil.<String, Object>builder()
                        .put("query", sanitizeMap(queryString, operationLog.sanitizeKeys()))
                        .put("body", sanitizeJson(requestBody, operationLog.sanitizeKeys())).build();
                operation.setRequestBody(JSONUtil.toJsonStr(requestParams));
            }

            //请求头
            if (operationLog.requestHeaderEnable()) {
                operation.setRequestHeader(getRequestHeader());
            }

            //业务类型
            if (operationLog.operateType() != null) {
                operation.setBusinessType(operationLog.operateType().getType());
            }

            //响应体
            if (operationLog.responseEnable()) {
                operation.setResponseBody(JSONUtil.toJsonStr(jsonResult));
            }

            //操作名称
            if ("".equals(operationLog.title())) {
                Operation operationAnnotation = handlerMethod.getMethodAnnotation(Operation.class);
                if (operationAnnotation != null) {
                    operation.setTitle(operationAnnotation.summary());
                }
            } else {
                operation.setTitle(operationLog.title());
            }


            // 设置浏览器类型
            UserAgentParser.UserAgentInfo userAgentInfo = UserAgentParser.parseUserAgent(httpServletRequest);
            operation.setBrowserType(userAgentInfo.getBrowser());
            operation.setOperatingSystem(userAgentInfo.getOs());

            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operation.setMethod(className + "." + methodName + "()");

            operation.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());

            sysLogOperationApi.save(operation);
        } catch (Exception err) {
            log.error("操作日志记录失败", err);
        } finally {
            TIME_THREADLOCAL.remove();
        }
    }

    /**
     * 获得当前用户的类型
     *
     * @param request 请求
     * @return 用户编号
     */
    public Integer getLoginUserType(HttpServletRequest request) {
        if (request == null) {
            return null;
        }
        // 2. 其次，基于 URL 前缀的约定
        if (request.getServletPath().startsWith(webProperties.getAdminApi().getPrefix())) {
            return UserTypeEnum.ADMIN.getValue();
        }
        if (request.getServletPath().startsWith(webProperties.getAppApi().getPrefix())) {
            return UserTypeEnum.MEMBER.getValue();
        }
        return null;
    }

    /**
     * 获取请求头
     *
     * @return String
     */
    private String getRequestHeader() {
        Map<String, String> headerMap = new LinkedHashMap<>();
        Enumeration<String> headerNames = httpServletRequest.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            //过滤token
            if (headerName.equals("Authorization".toLowerCase())) continue;
            String headerValue = httpServletRequest.getHeader(headerName);
            headerMap.put(headerName, headerValue);
        }
        return JSONUtil.toJsonStr(headerMap);
    }
}
